"""
build_mod_ic.py
Copyright 2015 Adam Greig
Licensed under the MIT licence, see LICENSE file for details.

Create a range of dual and quad SMD IC packages.

TODO:
    * Support other pad shapes, e.g. oval/half-oval
"""

from __future__ import print_function, division

# Other constants =============================================================

# Courtyard clearance
# Use 0.25 for IPC nominal and 0.10 for IPC least.
ctyd_gap = 0.25

# Courtyard grid
ctyd_grid = 0.05

# Courtyard line width
ctyd_width = 0.01

# Internal silk clearance from pads
silk_pad_igap = 0.5

# External silk clearance from pads
silk_pad_egap = 0.4

# Silk line width
silk_width = 0.15

# Internal silk pin1 arc radius
silk_pin1_ir = 0.6

# External two-row silk pin1 arc radius
silk_pin1_er = 0.3

# Fab layer line width
fab_width = 0.01

# Fab layer pin1 circle radius
fab_pin1_r = 0.4

# Fab layer pin1 corner offset
fab_pin1_offset = 0.8

# Ref/Val font size (width x height)
font_size = (1.0, 1.0)

# Ref/Val font thickness
font_thickness = 0.15

# Ref/Val font spacing from centre to top/bottom edge
font_halfheight = 0.7

# End Constants ===============================================================

import os
import sys
import time
import math
import subprocess
import argparse
import yaml
import fnmatch

from sexp import parse as sexp_parse, generate as sexp_generate
from kicad_mod import fp_line, fp_arc, fp_circle, fp_text, pad, draw_square, model


def pin_centres(conf):
    """
    Compute the locations of pin centres, (x, y).
    Generates centres for a 4-row chip, just ignore top/bottom rows if 2 rows.
    Returns (leftrow, bottomrow, rightrow, toprow).
    """

    # Handle non-square chips differently
    if "pins_first_row" in conf:
        v_pins_per_row = conf['pins_first_row']
        h_pins_per_row = (conf['pins'] - 2*v_pins_per_row) // 2
        hx = conf['row_pitch'][0] / 2.0
        vx = conf['row_pitch'][1] / 2.0
    else:
        h_pins_per_row = v_pins_per_row = conf['pins'] // conf['rows']
        hx = vx = conf['row_pitch'] / 2.0
    h_row_length = (h_pins_per_row - 1) * conf['pin_pitch']
    v_row_length = (v_pins_per_row - 1) * conf['pin_pitch']

    left_row = []
    bottom_row = []
    right_row = []
    top_row = []

    y = -v_row_length / 2.0
    for pin in range(v_pins_per_row):
        left_row.append((-hx, y))
        right_row.insert(0, (hx, y))
        y += conf['pin_pitch']
    y = -h_row_length / 2.0
    for pin in range(h_pins_per_row):
        top_row.insert(0, (y, -vx))
        bottom_row.append((y, vx))
        y += conf['pin_pitch']

    return left_row, bottom_row, right_row, top_row


def inner_apertures(ep, apertures):
    """
    Compute the position of apertures inside a pad,
    with:
        ep: (width, height) of pad
        apertures: (width, height, w_gap, h_gap) of apertures
    w_gap is the spacing in the x-axis between apertures.

    Fits as many apertures inside the pad as possible.

    Returns a list of (x,y) aperture centres.
    """
    out = []
    ep_w, ep_h = ep
    a_w, a_h, a_wg, a_hg = apertures

    n_x = int((ep_w - a_w) // (a_w + a_wg)) + 1
    n_y = int((ep_h - a_h) // (a_h + a_hg)) + 1

    x = -((n_x - 1)*(a_w + a_wg)/2.0)

    for ix in range(n_x):
        y = -((n_y - 1)*(a_h + a_hg)/2.0)
        for iy in range(n_y):
            out.append((x, y))
            y += a_h + a_hg
        x += a_w + a_wg

    return out


def exposed_pad(conf):
    """
    Compute pads for an exposed pad, which might have separate mask and paste
    apertures.
    """
    out = []
    ep_shape = conf['ep_shape']
    ep_layers = ["F.Cu"]
    ep_m_mask = None
    ep_m_paste = None

    # Mask apertures
    if "ep_mask_shape" not in conf:
        ep_layers.append("F.Mask")
        ep_m_mask = 0.001
    else:
        mask_shape = conf['ep_mask_shape']
        apertures = inner_apertures(ep_shape, mask_shape)
        layers = ["F.Mask"]
        for ap in apertures:
            out.append(pad("", "smd", "rect", ap, mask_shape[:2], layers,
                           m_mask=.001))

    # Paste apertures
    if "ep_paste_shape" not in conf:
        ep_layers.append("F.Paste")
        ep_m_paste = 0.001
    else:
        paste_shape = conf['ep_paste_shape']
        apertures = inner_apertures(ep_shape, paste_shape)
        layers = ["F.Paste"]
        for ap in apertures:
            out.append(
                pad("", "smd", "rect", ap, paste_shape[:2], layers,
                    m_paste=.001))

    # Vias
    if "ep_vias" in conf:
        v_d, v_s, v_g = conf['ep_vias']
        centres = inner_apertures(ep_shape, (v_s, v_s, v_g, v_g))
        layers = ["*.Cu"]
        for c in centres:
            p = pad("EP", "thru_hole", "circle", c, (v_s, v_s), layers,
                    drill=v_d)
            p.append(("zone_connect", 2))
            out.append(p)

    out.append(
        pad("EP", "smd", "rect", (0, 0),
            ep_shape, ep_layers, m_mask=ep_m_mask, m_paste=ep_m_paste))
    return out


def refs(conf):
    """Generate val and ref labels."""
    out = []

    if conf['rows'] == 2:
        ctyd_h = conf['chip_shape'][1] + 2 * ctyd_gap
    elif conf['rows'] == 4:
        # Handle non-square chips differently
        if isinstance(conf['row_pitch'], float):
            ctyd_h = conf['row_pitch'] + conf['pad_shape'][0] + 2 * ctyd_gap
        else:
            ctyd_h = conf['row_pitch'][1] + conf['pad_shape'][0] + 2 * ctyd_gap

    y = ctyd_h/2.0 + font_halfheight

    out.append(fp_text("reference", "REF**", (0, -y),
               "F.Fab", font_size, font_thickness))
    out.append(fp_text("value", conf['name'], (0, y),
               "F.Fab", font_size, font_thickness))

    return out


def fab(conf):
    """
    Generate a drawing of the chip on the Fab layer, including its outline,
    the outline of the pins, and a pin 1 indicator.
    """
    chip_w, chip_h = conf['chip_shape']
    pin_w, pin_h = conf['pin_shape']
    out = []

    # Chip outline
    nw, _, _, _, sq = draw_square(chip_w, chip_h, (0, 0), "F.Fab", fab_width)
    out += sq

    # Pin 1
    centre = (nw[0] + fab_pin1_offset, nw[1] + fab_pin1_offset)
    end = (centre[0], centre[1] + fab_pin1_r)
    out.append(fp_circle(centre, end, "F.Fab", fab_width))

    # Pins
    leftr, bottomr, rightr, topr = pin_centres(conf)
    idx = 1
    for pin in leftr:
        idx += 1
        if idx - 1 in conf.get('skip_pins', []):
            continue
        xy = -(chip_w + pin_w) / 2.0, pin[1]
        _, _, _, _, sq = draw_square(pin_w, pin_h, xy, "F.Fab", fab_width)
        out += [sq[0], sq[2], sq[3]]
    for pin in rightr:
        idx += 1
        if idx - 1 in conf.get('skip_pins', []):
            continue
        xy = (chip_w + pin_w) / 2.0, pin[1]
        _, _, _, _, sq = draw_square(pin_w, pin_h, xy, "F.Fab", fab_width)
        out += [sq[0], sq[1], sq[2]]
    if conf['rows'] == 4:
        for pin in topr:
            idx += 1
            if idx - 1 in conf.get('skip_pins', []):
                continue
            xy = pin[0], -(chip_h + pin_w) / 2.0
            _, _, _, _, sq = draw_square(pin_h, pin_w, xy, "F.Fab", fab_width)
            out += [sq[0], sq[1], sq[3]]
        for pin in bottomr:
            idx += 1
            if idx - 1 in conf.get('skip_pins', []):
                continue
            xy = pin[0], (chip_h + pin_w) / 2.0
            _, _, _, _, sq = draw_square(pin_h, pin_w, xy, "F.Fab", fab_width)
            out += [sq[1], sq[2], sq[3]]

    return out


def internal_silk(conf):
    """
    Generate an internal silkscreen, with an outline of the part and a pin1
    indicator.
    """
    out = []

    pins = conf['pins']
    rows = conf['rows']
    pin_pitch = conf['pin_pitch']
    row_pitch = conf['row_pitch']
    pad_shape = conf['pad_shape']

    width = row_pitch - pad_shape[0] - 2 * silk_pad_igap
    if rows == 2:
        height = (((pins / rows) - 1) * pin_pitch)
    elif rows == 4:
        height = width

    ir = silk_pin1_ir
    if ir > width:
        ir = width

    c = (0, 0)
    layer = "F.SilkS"
    nw, ne, se, sw, sq = draw_square(width, height, c, layer, silk_width)
    out.append(fp_line((nw[0] + ir, nw[1]), ne, layer, silk_width))
    out.append(fp_line(ne, se, layer, silk_width))
    out.append(fp_line(se, sw, layer, silk_width))
    out.append(fp_line(sw, (nw[0], nw[1] + ir), layer, silk_width))
    start = (nw[0], nw[1] + ir)
    end = (nw[0] + ir, nw[1])
    out.append(fp_line(start, end, "F.SilkS", silk_width))

    # Old circular pin1 indicator:
    # out += sq
    # start = nw
    # end = (start[0] + silk_pin1_ir, start[1])
    # out.append(fp_arc(start, end, 90, "F.SilkS", silk_width))

    return out


def external_silk(conf):
    """
    Generate an external silkscreen.
    For two row devices: Horizontal lines top and bottom, semicircle pin 1
    For four row devices: Three sharp corners and a cut corner for pin 1
    """
    out = []

    rows = conf['rows']
    chip_shape = conf['chip_shape']
    w = silk_width
    l = "F.SilkS"
    x = chip_shape[0]/2.0
    y = chip_shape[1]/2.0

    if rows == 2:
        r = silk_pin1_er
        out.append(fp_line((-x+2*r, -y), (x, -y), l, w))
        out.append(fp_line((-x, y), (x, y), l, w))
        out.append(fp_arc((-x+r, -y), (-x, -y), 180, l, w))
    elif rows == 4:
        if 'pins_first_row' in conf:
            v_pins_per_row = conf['pins_first_row']
            h_pins_per_row = (conf['pins'] - 2*v_pins_per_row) // 2
            pin_x = ((h_pins_per_row - 1) * conf['pin_pitch']) / 2.0
            pin_y = ((v_pins_per_row - 1) * conf['pin_pitch']) / 2.0
        else:
            pins_per_row = conf['pins'] / rows
            pin_x = pin_y = ((pins_per_row - 1) * conf['pin_pitch']) / 2.0
        dx = x - pin_x - silk_pad_egap
        dy = y - pin_y - silk_pad_egap

        # NW
        d_pin1 = min(dx, dy)
        out.append(fp_line((-x, -y+d_pin1), (-x+d_pin1, -y), l, w))
        # NE
        out.append(fp_line((x-dx, -y), (x, -y), l, w))
        out.append(fp_line((x, -y), (x, -y+dy), l, w))
        # SE
        out.append(fp_line((x-dx, y), (x, y), l, w))
        out.append(fp_line((x, y), (x, y-dy), l, w))
        # SW
        out.append(fp_line((-x+dx, y), (-x, y), l, w))
        out.append(fp_line((-x, y), (-x, y-dy), l, w))

    return out


def silk(conf):
    default = 'internal' if 'ep_shape' not in conf else 'external'
    silk = conf.get('silk', default)
    if silk == 'external':
        return external_silk(conf)
    elif silk == 'internal':
        return internal_silk(conf)
    else:
        return []


def ctyd(conf):
    row_pitch = conf['row_pitch']
    pad_w, pad_h = conf['pad_shape']
    chip_w, chip_h = conf['chip_shape']

    if conf['rows'] == 2:
        width = row_pitch + pad_w + 2 * ctyd_gap
        height = chip_h + 2 * ctyd_gap
    elif conf['rows'] == 4:
        # We need to handle non-square chips slightly differently,
        # depending on whether row_pitch is given as (w, h) or just a scalar.
        if isinstance(row_pitch, float):
            height = width = row_pitch + pad_w + 2 * ctyd_gap
        else:
            width = row_pitch[0] + pad_w + 2 * ctyd_gap
            height = row_pitch[1] + pad_w + 2 * ctyd_gap

    # Ensure courtyard lies on a specified grid
    # (double the grid since we halve the width/height)
    grid = 2*ctyd_grid
    width = grid * int(math.ceil(width / (2*ctyd_grid)))
    height = grid * int(math.ceil(height / (2*ctyd_grid)))

    _, _, _, _, sq = draw_square(width, height, (0, 0), "F.CrtYd", ctyd_width)
    return sq


def pad_row(centres, num, idx, shape, size, layers, skip):
    out = []
    for centre in centres:
        idx += 1
        if idx - 1 in skip:
            continue
        out.append(pad(num, "smd", shape, centre, size, layers))
        num += 1
    return num, idx, out


def pads(conf):
    out = []
    layers = ["F.Cu", "F.Mask", "F.Paste"]
    size_lr = conf['pad_shape']
    size_tb = size_lr[1], size_lr[0]
    shape = "rect"
    skip = conf.get('skip_pins', [])
    leftr, btmr, rightr, topr = pin_centres(conf)
    num = 1
    idx = 1

    num, idx, pins = pad_row(leftr, num, idx, shape, size_lr, layers, skip)
    out += pins

    if conf['rows'] == 4:
        num, idx, pins = pad_row(btmr, num, idx, shape, size_tb, layers, skip)
        out += pins

    num, idx, pins = pad_row(rightr, num, idx, shape, size_lr, layers, skip)
    out += pins

    if conf['rows'] == 4:
        num, idx, pins = pad_row(topr, num, idx, shape, size_tb, layers, skip)
        out += pins

    # Exposed pad (potentially with separate mask/paste apertures)
    if "ep_shape" in conf:
        out += exposed_pad(conf)

    return out


def _3d(conf):
    if "model" in conf:
        return [model(**conf["model"])]
    else:
        return []


def footprint(conf):
    tedit = format(int(time.time()), 'X')
    sexp = ["module", conf['name'], ("layer", "F.Cu"), ("tedit", tedit)]
    sexp += refs(conf)
    sexp += fab(conf)
    sexp += silk(conf)
    sexp += ctyd(conf)
    sexp += pads(conf)
    sexp += _3d(conf)
    return sexp_generate(sexp)


def git_version(libpath):
    args = ["git", "describe", "--abbrev=8", "--dirty=-dirty", "--always"]
    git = subprocess.Popen(args, cwd=libpath, stdout=subprocess.PIPE)
    return git.stdout.read().decode().strip()


def load_items(modpath):
    config = {}
    for dirpath, dirnames, files in os.walk(modpath):
        dirnames.sort()
        files.sort()
        for fn in fnmatch.filter(files, "*.yaml"):
            path = os.path.join(dirpath, fn)
            with open(path) as f:
                item = yaml.safe_load(f)
                item["path"] = dirpath
                config[item["name"]] = item
    return config


def main(prettypath, modpath, verify=False, verbose=False):
    config = load_items(modpath)
    for name, conf in config.items():
        conf['name'] = name
        assert conf['rows'] in (2, 4), \
            "Must have either two or four rows"
        assert conf['pins'] % conf['rows'] == 0, \
            "Pins must equally divide among rows"
        fp = footprint(conf)
        path = os.path.join(prettypath, name+".kicad_mod")

        if verify and verbose:
            print("Verifying", path)

        # Check if an identical part already exists
        if os.path.isfile(path):
            with open(path) as f:
                old = f.read()
            old = [n for n in sexp_parse(old) if n[0] != "tedit"]
            new = [n for n in sexp_parse(fp) if n[0] != "tedit"]
            if new == old:
                continue

        # If not, either verification failed or we should output the new fp
        if verify:
            return False
        else:
            with open(path, "w") as f:
                f.write(fp)

    # If we finished and didn't return yet, verification has succeeded.
    if verify:
        return True


if __name__ == "__main__":
    parser = argparse.ArgumentParser()
    parser.add_argument("prettypath", type=str, help=
                        "Path to footprints to process")
    parser.add_argument("modpath", type=str, help=
                        "Path to .yaml files defining footprints")
    parser.add_argument("--verify", action="store_true", help=
                        "Verify libraries are up to date")
    parser.add_argument("--verbose", action="store_true", help=
                        "Print out every library verified")
    args = vars(parser.parse_args())
    result = main(**args)
    if args['verify']:
        if result:
            print("OK: all footprints up-to-date.")
            sys.exit(0)
        else:
            print("Error: footprints not up-to-date.", file=sys.stderr)
            sys.exit(1)
